package utility;

import java.io.*;
import java.io.FileNotFoundException;
import java.lang.reflect.*;
import java.util.*;

import javax.xml.*;
import javax.xml.parsers.*;
import javax.xml.transform.*;
import javax.xml.transform.Result;
import javax.xml.transform.dom.*;
import javax.xml.transform.stream.*;
import javax.xml.validation.*;

import liquidfs.*;
import liquidfs.core.*;
import liquidfs.data.*;
import liquidfs.data.ex.*;
import liquidfs.data.ops.*;
import liquidfs.ui.net.*;

import org.w3c.dom.*;
import org.xml.sax.*;
import org.xml.sax.ErrorHandler;
import org.xml.sax.helpers.*;

import com.siemens.ct.exi.*;
import com.siemens.ct.exi.api.dom.*;
import com.siemens.ct.exi.api.sax.*;
import com.siemens.ct.exi.exceptions.*;
import com.siemens.ct.exi.grammars.*;
import com.siemens.ct.exi.helpers.*;
import com.thoughtworks.xstream.*;
import com.thoughtworks.xstream.annotations.*;
import com.thoughtworks.xstream.io.*;
import com.thoughtworks.xstream.mapper.*;

public class SchemaGenerator {
   
   private List<Class<?>> simpleTypeConverters = new ArrayList<Class<?>>();
   private Map<Class<?>, String> xsdTypeMapping = new HashMap<Class<?>, String>();
   private Map<Class<?>, Element> generated = new HashMap<Class<?>, Element>();
   private Document dom;
   private Mapper mapper = null;
   
   private static String ns = "http://www.w3.org/2001/XMLSchema";
      
   public SchemaGenerator (XStream xstream) throws ParserConfigurationException {
      
      xsdTypeMapping.put(int.class, "xs:integer");
      xsdTypeMapping.put(long.class, "xs:integer");
      xsdTypeMapping.put(short.class, "xs:integer");
      xsdTypeMapping.put(byte.class, "xs:integer");
      
      xsdTypeMapping.put(byte[].class, "xs:base64binary");
      xsdTypeMapping.put(boolean.class, "xs:boolean");
      xsdTypeMapping.put(double.class, "xs:double");
      xsdTypeMapping.put(float.class, "xs:float");
      
      xsdTypeMapping.put(String.class, "xs:string");
      xsdTypeMapping.put(Calendar.class, "xs:string");
      xsdTypeMapping.put(Date.class, "xs:string");
      
      this.mapper = xstream.getMapper();
      
      DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
      factory.setNamespaceAware(true);
      
      DocumentBuilder builder = factory.newDocumentBuilder();
      dom = builder.newDocument();
      Element root = dom.createElementNS(ns, "xs:schema");
      
      dom.appendChild(root);
   }
   
   private Element generateType (Class<?> clazz) {
      
      if (generated.containsKey(clazz) || clazz.equals(Object.class)) {
         return generated.get(clazz);
      }
      
      //XStreamAlias classAlias = clazz.getAnnotation(XStreamAlias.class);
      //String className = (classAlias != null ? classAlias.value() : clazz.getName());
      String className = mapper.serializedClass(clazz);
      
      // If there are any sub-elements, this is a complex type.
      Element element = dom.createElementNS(ns, "xs:complexType");
      element.setAttribute("name", className);
      generated.put(clazz, element);
      
      // Sequence
      Element sequenceElement;
      
      // Determine the superclass
      Class<?> superclass = clazz.getSuperclass();
      
      // Collections are a special case
      if (Collection.class.isAssignableFrom(clazz)) {
         sequenceElement = dom.createElementNS(ns, "xs:sequence");
         element.appendChild(sequenceElement);
         
         Element elementElement = dom.createElementNS(ns, "xs:any");
         elementElement.setAttribute("minOccurs", "0");
         elementElement.setAttribute("maxOccurs", "unbounded");
         sequenceElement.appendChild(elementElement);
      }
      // Generate a type for the parent class (if this isn't Object)
      else if (superclass != null && superclass != Object.class)
      {
         Element superclassType = generateType(superclass);
         dom.getDocumentElement().appendChild(dom.createComment(" Type " + superclassType.getAttribute("name") + " "));
         dom.getDocumentElement().appendChild(superclassType);
      
         // Create the complexContent element
         Element complexContentElement = dom.createElementNS(ns, "xs:complexContent");
         element.appendChild(complexContentElement);
         
         // Declare this class as a subclass
         Element extensionElement = dom.createElementNS(ns, "xs:extension");
         extensionElement.setAttribute("base", superclassType.getAttribute("name"));
         complexContentElement.appendChild(extensionElement);
         
         // Create the sequence element
         sequenceElement = dom.createElementNS(ns, "xs:sequence");
         extensionElement.appendChild(sequenceElement);
      }
      else {
         // Create the sequence element
         sequenceElement = dom.createElementNS(ns, "xs:sequence");
         element.appendChild(sequenceElement);
      }
      
      // Determine if there are any non-attribute sub-elements
      for (Field field : clazz.getDeclaredFields()) {
         
         if (Modifier.isStatic(field.getModifiers()))
            continue;
         
         XStreamAsAttribute attribute = field.getAnnotation(XStreamAsAttribute.class);
         XStreamConverter converter = field.getAnnotation(XStreamConverter.class);
         XStreamOmitField omit = field.getAnnotation(XStreamOmitField.class);
         XStreamSchemaDefault defaultValue = field.getAnnotation(XStreamSchemaDefault.class);
         
         if (omit != null)
            continue;
         
         Class<?> type = field.getType();
         String name = mapper.serializedMember(clazz, field.getName());
         
         if (attribute != null && xsdTypeMapping.containsKey(type)) {
            Element attributeElement = dom.createElementNS(ns, "xs:attribute");
            attributeElement.setAttribute("name", name);
            attributeElement.setAttribute("type", xsdTypeMapping.get(type));
            attributeElement.setAttribute("use", "required");
            
            if (defaultValue != null)
               attributeElement.setAttribute("default", defaultValue.value());
               
            sequenceElement.getParentNode().appendChild(attributeElement);
         }
         else {
            Element elementElement = dom.createElementNS(ns, "xs:element");
            elementElement.setAttribute("name", name);
            sequenceElement.appendChild(elementElement);
            
            if (!type.isInterface() && !type.equals(Object.class)) {
               if (!xsdTypeMapping.containsKey(type)) {
                  Element typeElement = generateType(type);
                  elementElement.setAttribute("type", typeElement.getAttribute("name"));
                  dom.getDocumentElement().appendChild(dom.createComment(" Type " + typeElement.getAttribute("name") + " "));
                  dom.getDocumentElement().appendChild(typeElement);
               }
               else {
                  elementElement.setAttribute("type", xsdTypeMapping.get(type));
               }
            }
            
            if (defaultValue != null)
               elementElement.setAttribute("default", defaultValue.value());
         }
      }
      
      return element;
   }
   
   public void addClass (Class<?> clazz) {
      String className = mapper.serializedClass(clazz);
      
      Element root = dom.getDocumentElement();
      
      Element topElement = dom.createElementNS(ns, "xs:element");
      topElement.setAttribute("name", className);
      
      if (root.getFirstChild() != null) {
         root.insertBefore(topElement, root.getFirstChild());
         root.insertBefore(dom.createComment(" Element " + topElement.getAttribute("name") + " "), topElement);
      }
      else {
         root.appendChild(topElement);
      }
      
      Element typeElement = generateType(clazz);
      topElement.setAttribute("type", typeElement.getAttribute("name"));
      root.appendChild(dom.createComment(" Type " + typeElement.getAttribute("name") + " "));
      root.appendChild(typeElement);
   }
   
   public Document getSchema () {
      return this.dom;
   }
   
   public InputStream getSchemaInputStream () throws TransformerConfigurationException, TransformerException, TransformerFactoryConfigurationError {
      
      ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
      Source xmlSource = new DOMSource(dom);
      Result outputTarget = new StreamResult(outputStream);
      TransformerFactory.newInstance().newTransformer().transform(xmlSource, outputTarget);
      InputStream is = new ByteArrayInputStream(outputStream.toByteArray());
      
      return is;
   }
}
